#include <ctype.h>
#include <libgen.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <linux/hdreg.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <net/if.h>
#include <unistd.h>
#include <sys/types.h>
#include <ifaddrs.h>
#include <mntent.h>
#include <sys/stat.h>
#include <dirent.h>
#include <syslog.h>

#include <glib.h>

#include "hardware_info.h"


#define PROC_MOUNTS "/proc/mounts"
#define PROC_DISKSTATS "/proc/diskstats"

#define DMI_UUID_FILE "/sys/devices/virtual/dmi/id/product_uuid"

#define CMDLINE_FILE "/proc/cmdline"

#define BONDING_DIR_PATH "/proc/net/bonding"

#define BUF_SIZE 1024
#define SERIAL_NO_LEN 20
#define MODEL_LEN 40


/*
 * harddisk_id, get harddisk information from disk name
 */
static char* harddisk_id(const char* disk_name);

/*
 * root_device, get root device name
 */
static char* root_device();

/*
 * _is_block_device, check file with dev_name if a block device
 * return 1 if yes, or 0 if not
 */
static int _is_block_device(const char* dev_name);

/*
 * file_stat, check if file matches mode
 * return 1 if contain, or 0 if not
 */
int file_stat(const char* file_name, mode_t mode)
{
    struct stat buf;

    if(stat(file_name, &buf) == -1)
        return 0;

    return (buf.st_mode & mode);
}

/*
 * text_file_get_contents, get contents of file with path file_name
 * file_name: file path
 * return content of file with path file_name
 */
static char* text_file_get_contents(const char* file_name)
{
    GError* local_error = NULL;
    char* result = NULL;
    char* buf = NULL;

    // buf = (char*)malloc(BUF_SIZE);
    // if(!buf)
    //     return NULL;

    // memset(buf, 0 , BUF_SIZE);

    if(!g_file_get_contents(file_name, &buf, NULL, &local_error))
        return NULL;

    result = strdup(buf);

    free(buf);
    return result;
}

/*
 * _remove_line_break, remove '\n' in str
 */
static void _remove_line_break(char* str)
{
    char* p;

    p = str;

    while(*p != '\0')
    {
        if(*p == '\n')
        {
            *p = '\0';
            break;
        }
        ++p;
    }
}

/*
 * command_line_get_contents, get content of command line file, and remove '\n' in content
 * file_name: command line file path
 */
static char* command_line_get_contents(const char* file_name)
{
	char* cmdline = NULL;

	cmdline = text_file_get_contents(file_name);

	if(cmdline)
		_remove_line_break(cmdline);

	return cmdline;
}

/*
 * command_line_get_root, get root device from cmdline
 * cmdline: content of cmdline file
 */
static char* command_line_get_root(const char* cmdline)
{
	char copy[BUF_SIZE];
	char* delim = " \t";
	char* str = NULL;
	char* result = NULL;

	if(!cmdline)
		return NULL;

	memset(copy, 0, BUF_SIZE);
	memcpy(copy, cmdline, strlen(cmdline) + 1);

	str = strtok(copy, delim);
	if(!str)
		return NULL;

	if(strncmp(str, "root=", 5) == 0)
		return strdup(str);

	while((str = strtok(NULL, delim)) != NULL)
	{
		if(strncmp(str, "root=", 5) == 0) {
			result = strdup(str);
            break;
        }
	}

	return result;
}

/*
 * _root_label_convert, convert conv_from in src to conv_to and put content to dest
 * dest: destination string
 * src: source string
 * conv_from: content to convert
 * conv_to: content to convert to
 */
static void _root_label_convert(char* dest, char* src, char* conv_to, char* conv_from)
{
	char* p = NULL;
	char* q = NULL;
	char copy[BUF_SIZE];
	int to_length = 0;
	int from_length = 0;
	int count = 0;


	if(!dest || !src || !conv_to || !conv_from)
		return ;

	to_length = strlen(conv_to);
	from_length = strlen(conv_from);

	memset(copy, 0, BUF_SIZE);
	memcpy(copy, src, strlen(src));

	q = copy;

	while((p = strstr(q, conv_from)) != NULL)
	{
		*p = '\0';

		sprintf(&dest[count], "%s%s", q, conv_to);
		count += p - q + to_length;

		if(('\0' == *(p + from_length)))
		{
			q = p + from_length;
			break;
		}

		q = p + from_length;
	}

	if('\0' != *q)
		sprintf(&dest[count], "%s", q);
}

/*
 * command_line_root_get_device_name, get device name from cmdline_root
 */
static char* command_line_root_get_device_name(const char* cmdline_root)
{
	char* device_name = NULL;
	char copy[BUF_SIZE];
	char buf[BUF_SIZE];
	char* str = NULL;
	char* delim = "=";

	if(!cmdline_root)
		return NULL;

	memset(copy, 0, BUF_SIZE);
	memset(buf, 0, BUF_SIZE);
	memcpy(copy, cmdline_root, strlen(cmdline_root) + 1);

	str = strtok(copy, delim);
	if(!str)
		return NULL;

	while((str = strtok(NULL, delim)) != NULL)
	{
		char* tmp = NULL;
		char converted[BUF_SIZE];

		if(strcmp(str, "UUID") == 0)
		{
			tmp = strtok(NULL, delim);
			if(!tmp)
				return NULL;
			sprintf(buf, "/dev/disk/by-uuid/%s", tmp);
			device_name = strdup(buf);
            break;
		}
		else if(strcmp(str, "LABEL") == 0)
		{
			tmp = strtok(NULL, delim);
			if(!tmp)
				return NULL;

			memset(converted, 0, BUF_SIZE);
			_root_label_convert(converted, tmp, "\\x2f", "/");

			sprintf(buf, "/dev/disk/by-label/%s", converted);

			device_name = strdup(buf);
            break;
		}
		else {
			device_name = strdup(str);
            break;
        }
	}

	return device_name;
}

/*
 * command_line_get_root_device_name, get root device name from cmdline file
 */
static char* command_line_get_root_device_name(const char* file_name)
{
	char* cmdline = NULL;
	char* root = NULL;
	char* root_device_name = NULL;

	cmdline = command_line_get_contents(file_name);

	if(!cmdline)
		return NULL;

	root = command_line_get_root(cmdline);
	if(!root)
		goto command_out;

	root_device_name = command_line_root_get_device_name(root);

command_out:
	if(cmdline)
		free(cmdline);
	if(root)
		free(root);

	return root_device_name;
}

struct block_device
{
    char* name;
    char* id;
};


struct network_interface
{
    char* name;
    char* mac;
};

/*
 * interface_compare, compare interface's mac
 * return 0 if two mac is the same, number below if a > b, number above 0 if a < b
 */
static gint interface_compare(gconstpointer a, gconstpointer b)
{
    struct network_interface *if_a, *if_b;
    if_a = (struct network_interface *)a;
    if_b = (struct network_interface *)b;

    if(if_a && if_b && if_a->mac && if_b->mac)
        return - strcmp(if_a->mac, if_b->mac);

    return 0;
}

/*
 * network_interface_free,  free network_interface struct
 */
static void network_interface_free(gpointer data, gpointer user_data)
{
    struct network_interface* iface = NULL;
    iface = (struct network_interface*)data;

    if(iface)
    {
        if(iface->name)
            free(iface->name);
        if(iface->mac)
            free(iface->mac);

        free(iface);
    }
}

/*
 * network_interface_create, create a object of struct network_interface
 */
static struct network_interface* network_interface_create(char* iface_name, char* mac_address)
{
    struct network_interface *iface;
    char* name;
    char* mac;

    iface = (struct network_interface*)malloc(sizeof(struct network_interface));
    if(!iface)
        return 0;

    name = strdup(iface_name);
    if(!name)
    {
        free(iface);
        return NULL;
    }

    mac = strdup(mac_address);
    if(!mac)
    {
        free(iface);
        free(name);
        return NULL;
    }
    iface->name = name;
    iface->mac = mac;

    return iface;
}


/*
 * network_interface_get_mac, get the mac string of a interface from interface name
 */
static char* network_interface_get_mac(const char* interface_name)
{
    int fd = -1;
    struct ifreq ifr;
    unsigned char *mac;
    char* str;
    int i;

    char buf[BUF_SIZE];
    memset(buf, 0, BUF_SIZE);
    fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(fd < 0)
        return NULL;

    ifr.ifr_addr.sa_family = AF_INET;
    strncpy(ifr.ifr_name, interface_name, IFNAMSIZ - 1);

    if(ioctl(fd, SIOCGIFHWADDR, &ifr))
    {
        close(fd);
        return NULL;
    }

    close(fd);

    mac = (unsigned char *)ifr.ifr_hwaddr.sa_data;

    sprintf(buf, "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x" , mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);

    str = strdup(buf);

    for(i = 0; i < strlen(str); i++)
    {
        str[i] = tolower(str[i]);
    }

    return str;

}


/*
 * remove_new_line_char, remove '\n' in string and replace it with '\0'.
 */
static void remove_new_line_char(char* buf)
{
    char* p = buf;

    if(!buf)
        return;

    while(*p != '\0')
    {
        if(*p == '\n')
        {
            *p = '\0';
            break;
        }
        p++;
    }
}

/*
 * string_to_lower, convert character in string to lower form.
 */
static void string_to_lower(char* str)
{
    int i;
    int len;

    if(!str)
        return;

    len = strlen(str);
    if(len == 0)
        return;

    for(i = 0; i < len; i++)
    {
        str[i] = tolower(str[i]);
    }
}

/*
 * bonding_file_get_network_interface_list, get network interface information from a bonding file.
 */
static GList* bonding_file_get_network_interface_list(const char* path)
{
    FILE *fp = NULL;
    char buf[BUF_SIZE] = {0};
    GList *list = NULL;
    char iface_name[128] = {0};
    char iface_mac[128] = {0};
    struct network_interface *iface = NULL;


    fp = fopen(path, "r");

    if(!fp)
    {
        return NULL;
    }

    while(fgets(buf, BUF_SIZE, fp))
    {
        memset(iface_name, 0, sizeof(iface_name));
        if(sscanf(buf, "Slave Interface: %s", iface_name) == 1)
        {
            memset(iface_mac, 0, sizeof(iface_mac));

            remove_new_line_char(iface_name);
            while(fgets(buf, BUF_SIZE, fp))
            {
                if(sscanf(buf, "Permanent HW addr: %s", iface_mac) == 1)
                {
                    remove_new_line_char(iface_mac);
                    string_to_lower(iface_mac);
                    break;
                }
            }

            if((strlen(iface_name) > 0) && (strlen(iface_mac) > 0))
            {
                iface = network_interface_create(iface_name, iface_mac);
                if(iface)
                {
                    list = g_list_append(list, (gpointer)iface);
                }
            }
        }
    }

    fclose(fp);

    return list;
}

/*
 * bonding_get_network_interface_list, get network interface information from bonding.
 */
static GList* bonding_get_network_interface_list()
{
    DIR *dp = NULL;
    GList* list = NULL;
    GList* l;
    struct dirent *dir = NULL;
    char path[BUF_SIZE] = {0};
    struct stat sb;

    dp = opendir(BONDING_DIR_PATH);
    if(!dp)
        return NULL;

    while((dir = readdir(dp)) != NULL)
    {
        if((strcmp(dir->d_name, ".") == 0) || (strcmp(dir->d_name, "..") == 0))
            continue;

        sprintf(path, "%s/%s", BONDING_DIR_PATH, dir->d_name);

        if(stat(path, &sb) != 0)
            continue;

        if(!S_ISREG(sb.st_mode))
            continue;

        l = bonding_file_get_network_interface_list(path);
        if(l)
        {
            list = g_list_concat(list, l);
        }
    }

    closedir(dp);

    return list;
}

/*
 * network_interface_list_create, create a list for network interface devices
 */
static GList* network_interface_list_create()
{
    GList* list = NULL;
    GList* bonding_list = NULL;
    struct ifaddrs *addrs, *padd;
    char* mac;
    struct network_interface* iface;    

    if(getifaddrs(&addrs) == -1)
        return NULL;

    padd = addrs;

    while(padd)
    {
        if(padd->ifa_addr && (
            (padd->ifa_addr->sa_family == AF_INET) ||
            (padd->ifa_addr->sa_family == AF_INET6) ||
            (padd->ifa_addr->sa_family == AF_PACKET)
            ))
        {
            if(padd->ifa_name && (
                    (strncmp(padd->ifa_name, "eth", 3) == 0) ||
                    (strncmp(padd->ifa_name, "en",  2) == 0)
                ))
            {
                mac = network_interface_get_mac(padd->ifa_name);
                if(mac)
                {
                    iface = network_interface_create(padd->ifa_name, mac);
                    free(mac);
                    mac = NULL;

                    if(iface)
                    {
                        list = g_list_append(list, (gpointer)iface);
                    }
                }
            }
        }
        padd = padd->ifa_next;
    }

    if(addrs)
    {
        freeifaddrs(addrs);
    }

    bonding_list = bonding_get_network_interface_list();

    if(bonding_list)
    {
        list = g_list_concat(list, bonding_list);
    }

    return list;
}


/*
 * network_interface_list_free, free network interface list
 */
static void network_interface_list_free(GList* list)
{
    g_list_foreach(list, network_interface_free, NULL);
    g_list_free(list);
}

/*
 * network_interface_get_max_mac, reorder network interface list and get interface for the max mac
 */
static char* network_interface_get_max_mac()
{
    GList* list;
    GList* first;
    char* mac = NULL;
    struct network_interface* iface;

    list = network_interface_list_create();
    list = g_list_sort(list, interface_compare);

    first = g_list_first(list);

    if(first)
    {
        iface = (struct network_interface*)(first->data);
        if(iface && iface->mac)
        {
            mac = strdup(iface->mac);
        }
    }
    network_interface_list_free(list);
    return mac;
}

/*
 * system_uuid_from_sysfs, get system uuid from sys file system
 */
static char* system_uuid_from_sysfs(const char* file_name)
{
    FILE * fp = NULL;
    char buf[BUF_SIZE];
    int i;

    fp = fopen(file_name, "r");
    if(!fp)
        return NULL;

    memset(buf, 0, BUF_SIZE);

    if(!fgets(buf, BUF_SIZE, fp))
    {
        fclose(fp);
        return NULL;
    }

    for(i = 0; i < BUF_SIZE; i++)
    {
        if((buf[i] == '\n') || (buf[i] == '\r'))
            buf[i] = '\0';
    }
    buf[BUF_SIZE - 1] = '\0';

    fclose(fp);

    for (i = 0; i < strlen(buf); i++)
    {
        buf[i] = tolower(buf[i]);
    }


    return strdup(buf);
}

/*
 * system_uuid_from_dmidecode, get system uuid from dmidecode
 */
static char* system_uuid_from_dmidecode()
{
    FILE* fp;
    char buf[BUF_SIZE];
    int i;

    if(access("/usr/sbin/dmidecode", F_OK | R_OK | X_OK) != 0)
        return NULL;

    fp = popen("/usr/sbin/dmidecode -s system-uuid | grep '[0-9a-fA-F]\\{8\\}-[0-9a-fA-F]\\{4\\}-[0-9a-fA-F]\\{4\\}-[0-9a-fA-F]\\{4\\}-[0-9a-fA-F]\\{12\\}'", "r");
    if(!fp)
        return NULL;

    memset(buf, 0, BUF_SIZE);
    if(!fgets(buf, BUF_SIZE, fp))
    {
        pclose(fp);
        return NULL;
    }

    for(i = 0; i < BUF_SIZE; i++)
    {
        if((buf[i] == '\n') || (buf[i] == '\r'))
            buf[i] = '\0';
    }
    buf[BUF_SIZE - 1] = '\0';

    pclose(fp);

    for (i = 0; i < strlen(buf); i++)
    {
        buf[i] = tolower(buf[i]);
    }

    return strdup(buf);
}

/*
 * system_uuid, get system uuid
 */
static char* system_uuid()
{
    char* sys_uuid = NULL;

    sys_uuid = system_uuid_from_sysfs(DMI_UUID_FILE);

    if(sys_uuid)
        return sys_uuid;

    sys_uuid = system_uuid_from_dmidecode();
    
    return sys_uuid;
}


/*
 * _is_block_device, check file with dev_name if a block device
 * return 1 if yes, or 0 if not
 */
static int _is_block_device(const char* dev_name)
{
    struct stat sb;

    if(!dev_name)
        return 0;

    if(stat(dev_name, &sb) == -1)
        return 0;

    return ((sb.st_mode & S_IFMT) == S_IFBLK);
}


/*
 * _root_device_from_proc_cmdline, get root device name from cmdline file
 */
static char* _root_device_from_proc_cmdline(const char* file_name)
{
    return command_line_get_root_device_name(file_name);
}

/*
 * _root_device_from_proc_mounts, get root device name from /proc/mounts
 */
static char* _root_device_from_proc_mounts()
{
    char* root_dev = NULL;
    struct mntent* ent;
    FILE *f;

    if(!file_stat(PROC_MOUNTS, S_IRUSR))
        return NULL;

    f = setmntent(PROC_MOUNTS, "r");
    if(NULL == f)
        return NULL;

    while(NULL != (ent = getmntent(f)))
    {
        if((strcmp(ent->mnt_dir, "/") == 0) && (strcmp(ent->mnt_fsname, "rootfs") != 0))
            break;
    }

    if(ent && ent->mnt_fsname)
        root_dev = strdup(ent->mnt_fsname);
    else
        root_dev = NULL;

//    if(root_dev)
//        printf("%s\n", root_dev);

    endmntent(f);

    return root_dev;
}

/*
 * root_device, get root device name
 */
static char* root_device()
{
    char* root_dev_name = NULL;

    root_dev_name = _root_device_from_proc_mounts();

    if(root_dev_name && _is_block_device(root_dev_name))
        return root_dev_name;

    if(root_dev_name)
    {
        free(root_dev_name);
        root_dev_name = NULL;
    }

    root_dev_name = _root_device_from_proc_cmdline(CMDLINE_FILE);

    if(root_dev_name && _is_block_device(root_dev_name))
        return root_dev_name;

    if(root_dev_name)
        free(root_dev_name);

    return NULL;
}

/*
 * harddisk_id_sata, get sata harddisk information from disk name
 */
static char* harddisk_id_sata(const char* disk_name)
{
    struct hd_driveid id;
    char serial_no[BUF_SIZE];
    char model[BUF_SIZE];
    char buf[2 * BUF_SIZE];
    int fd = -1;

    memset(serial_no, 0, BUF_SIZE);
    memset(model, 0, BUF_SIZE);
    memset(buf, 0, 2 * BUF_SIZE);

    fd = open(disk_name, O_RDONLY | O_NONBLOCK);

    if(fd < 0)
    {
        return NULL;
    }

    if(!ioctl(fd, HDIO_GET_IDENTITY, &id))
    {
        int i, j;

        j = 0;
        for(i = 0; i < SERIAL_NO_LEN; i++)
        {
            if(isgraph(id.serial_no[i]))
            {
                serial_no[j] = id.serial_no[i];
                j++;
            }
        }

        j = 0;
        for(i = 0; i < MODEL_LEN; i++)
        {
            if(isgraph(id.model[i]))
            {
                model[j] = id.model[i];
                j++;
            }
        }
        sprintf(buf, "%s_%s", model, serial_no);
    } else {
        close(fd);
        return NULL;
    }

    close(fd);

    return strdup(buf);
}

/*
 * nvme_get_information, get nvme device information
 */
static int nvme_get_information(const char* device, const char* info, char* buff, size_t buf_size)
{
    FILE* fp = NULL;
    char path[BUF_SIZE] = {0};
    char content[BUF_SIZE] = {0};
    int ret;
    int result = 0;
    int i = 0;
    int j = 0;
    char* p;
    int has_graph = 0;
    int content_length = 0;
    char* pa = NULL;

    if (!device || strlen(device) == 0 || !info || strlen(info) == 0)
    {
        return -1;
    }

    // sprintf(path, "/sys/block/%s/device/%s", device, info);
    asprintf(&pa, "/sys/block/%s/device/%s", device, info);
//    syslog(LOG_INFO,"path : [%s]",pa);

    fp = fopen(pa, "r");
    if (fp == NULL)
    {
        return -1;
    }

    ret = fread(content, 1, sizeof(content), fp);
    if (ret <= 0)
    {
        result = -1;
        goto out;
    }

    p = content + strlen(content);
    while (p != content)
    {
        if (!isgraph(*p))
        {
            *p = '\0';
        } else {
            break;
        }
        p--;
    }

    i = 0;
    j = 0;
    content_length = strlen(content);
    for (i = 0; i < content_length && j < buf_size; ++i)
    {
        if (isgraph(content[i]))
        {
            buff[j] = content[i];
            j++;
            has_graph = 1;
        } else {
            if (has_graph)
            {
                buff[j] = '-';
                j++;
                has_graph = 0;
            }
        }
    }

out:

    fclose(fp);

    return result;
}

/*
 * harddisk_id_nvme, get nvme harddisk information from disk name
 */
static char* harddisk_id_nvme(const char* partition_name)
{
    DIR *dp = NULL;
    struct dirent *dir = NULL;
    char buff[BUF_SIZE * 2] = {0};
    char serial_no[BUF_SIZE] = {0};
    char model[BUF_SIZE] = {0};
    char nvme_device[BUF_SIZE] = {0};

    dp = opendir("/sys/block");
    if (!dp)
    {
        return NULL;
    }

    while ((dir = readdir(dp)) != NULL)
    {
        if (strncmp(dir->d_name, partition_name, strlen(dir->d_name)) == 0)
        {
            strcpy(nvme_device, dir->d_name);
            break;
        }
    }

    closedir(dp);

    if (nvme_get_information(nvme_device, "model", model, sizeof(model)) < 0 ||
        nvme_get_information(nvme_device, "serial", serial_no, sizeof(serial_no)) < 0)
    {
        return NULL;
    }

    sprintf(buff, "%s_%s", model, serial_no);

    return strdup(buff);
}

/*
 * harddisk_id, get harddisk information from disk name
 */
static char* harddisk_id(const char* disk_name)
{
    char* result = NULL;
    char resolved_path[4096] = {0};
    char* file_name = NULL;

    result = realpath(disk_name, resolved_path);
    if (result == NULL)
    {
        return NULL;
    }

    if (!_is_block_device(resolved_path))
    {
        return NULL;
    }

    file_name = basename(resolved_path);
    if (strncmp(file_name, "nvme", strlen("nvme")) == 0)
    {
        return harddisk_id_nvme(file_name);
    }

    return harddisk_id_sata(disk_name);
}

char* hardware_info_harddisk_id()
{
    char* rootfs_dev = NULL;
    char* hd_id = NULL;

    rootfs_dev = root_device();
    if (rootfs_dev) {
        hd_id = harddisk_id(rootfs_dev);
        free(rootfs_dev);
        rootfs_dev = NULL;
    }
    
    return hd_id;
}

char* hardware_info_system_uuid()
{
    return system_uuid();
}

char* hardware_info_network_interface_mac()
{
    return network_interface_get_max_mac();
}
